C#进阶(中)

委托和事件

委托

委托是什么

委托是函数的容器,可以理解为表示函数的变量类型,用来储存、传递函数。
委托的本质是一个类,用来定义函数的类型,不同的函数必须对应和各自格式一致的委托。

语法

1
2
3
访问修饰符默认不写是public
public delegate 返回值 委托名(参数列表);
一般写在namespace语句块中

定义

1
2
3
delegate void MyFun();
public delegate int MyFun2(int x);//定义了一个规则,并没有使用。
//委托规则的声明不允许重名

使用自定义委托

委托变量是函数的容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MyFun f = new MyFun(Fun);//只能存储
f.Invoke();//执行函数

MyFun f2 = Fun;
f2();

MyFun f3 = Fun2;
int x = f3(114514);
static void Fun()
{

}
static int Fun2(int x)
{
return x;
}

委托常用:作为类成员;作为函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test
{
public MyFun fun;
public MyFun2 fun2;
public void TestFun(MyFun fun, MyFun2 fun2)
{
//先处理别的逻辑,当这些逻辑处理完了,在执行传入的函数
fun();
fun2(114514);
}
}
Test t = new Test();
t.TestFun(Fun, Fun2);

委托变量可以存储多个函数(多播委托)

1
2
3
4
5
6
7
8
9
10
//增加
//直接使用运算符相加
MyFun ff = Fun;
ff += Fun;
ff();

//删
//直接运算符相减
ff -= Fun;
ff();

系统定义好的委托

1
2
3
4
5
6
7
8
9
using System;
Action//无参无返回值的委托
Action action = Fun;

Func<string>//无参数可返回的泛型委托,返回类型可以改变。
Func<int> func = Fun2;

Action<int,string,...> act = Funx;//有参数有返回值的泛型委托。
//最多可以传16个参数

事件

事件是什么

事件是基于委托的存在,事件是委托的安全包裹,让委托的使用更有安全性。事件是一种特殊的变量类型。

事件的使用

事件是作为成员变量存在于类中的,委托怎么用事件就怎么用。但是事件不能在类外部调用,赋值。
注意,它只能作为成员存在于类和接口以及结构体中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test
{
//委托成员变量
public Action myFun;
//事件成员变量
public event Action myEvent;
public void Test()
{
myFun = Fun;
myEvent = Fun;
}
//运算也可以进行
myEvent += Fun;
muFun += Fun;
}

虽然不能在外面赋值,但是可以进行加减来添加移除。只能在类的内部封装调用。

为什么有事件

  • 防止外部随意置空委托
  • 防止外部随意调用委托
  • 事件相当于对委托进行了一次封装,使其更安全。

匿名函数

什么是匿名函数

没有名字的函数,匿名函数的使用主要是配合委托和事件使用,脱离委托和事件不会使用匿名函数

语法

使用关键字delegate修饰函数

1
2
3
4
delegate()
{

};

何时使用:函数中传递委托参数时,委托或事件赋值时。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//此处仅仅是声明函数
//无参匿名函数
Action a = delegate()
{
//此处写匿名函数逻辑,但是不能脱离委托和事件

};
a();//调用委托就会直接调用该匿名函数
//有参匿名函数
Action<int, string> b = delegate(int a, string b)
{

}
b(1145,"宇智波");
//有返回值的匿名函数
Func<int> c = delegate()
{
return 1;
}
c();
//一般情况会作为函数参数传递 或者 作为函数返回值
Test t = new Test();
t.Dos(1145, delegate()//这里相当于先存一个函数在传入,但是我们可以用匿名函数直接传入函数
{
Console.WriteLine();
}
)
class Test
{
public Action action;
public void Dos(int a, Action fun)
{
fun();
}
public Action GetFun()
{
return delegate()
{

};
}
}

匿名函数的缺点

添加到委托或事件容器后,不记录,无法单独移除。因为匿名函数没有名字,在进行委托的加减的时候无法自由删除匿名函数。一般删除时,只能将委托清空。

因此我们在使用匿名函数的时候,一般都是在使用只有一个匿名函数的时候可以使用匿名函数,这样可以随时清空也不会移除其他函数。

Lambda表达式

什么是Lambda表达式

可以把lambda表达式理解为匿名函数的简写,除了写法不同以外,使用上和匿名函数一模一样,二者都是与委托和事件配合使用的。

Lambda表达式语法

1
2
3
4
(参数列表) =>
{
函数体
}

Lambda表达式使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//无参
Action a = () =>
{
Console.WriteLine("无参无返回值的表达式");
};
a();
//有参
Action<int> b = (int a2) =>
{
Console.WriteLine("有参无返回值的表达式", a2);
};
b(114514);
//参类型可以省略,参数类型和委托或事件容器一致
Action<int> c = (a2) =>
{
Console.WriteLine("有参无返回值的表达式", a2);
};
c(114514);
//有返回值
Func<string, int> d = (value) =>
{
Console.WriteLine("有参数有返回值的表达式{0}", value);
return 1;
};
Console.WriteLine(d("宇智波"));

其他使用上与匿名函数一样,包括缺点都与匿名函数一致。

闭包

内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止了。
注意:该变量提供的值并非变量创造时的值,而是在父函数氛围内的最终值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Test
{
public event Action action;

public Test()
{
int value = 114514;
//外层,此时value一直不会被释放,除非被置空
action = () =>
{
//内层,此处形成闭包。当构造函数执行完毕后,原本应该被释放掉的临时变量value寿命延长了。
Console.WriteLine(value);
};
for(int i = 0; i < n; i++)
{
action += () =>
{
Console.WriteLine(i);//该变量提供的值并非临时变量,而是最终值,因此在最后存入action的值都是i循环最终的值,也就是10。而不是每一次循环过程中i的值。
};
}
}
}

List排序

List自带的排序方法

1
2
3
4
5
6
7
8
9
List<int> list = new List<int>();
list.Add(1);
list.Add(1);
list.Add(4);
list.Add(5);
list.Add(1);
list.Add(4);
list.Sort();//此为list自带的排序,默认为升序排列
//ArrayList中也有对应的排序方法。

自定义类的排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Item : IComparable<Item>
{
public int money;
public Item(int money)
{
this.money = money;
}
public int CompareTo(Item other)
{
//返回值含义,如果小于0,放在传入对象的前面。
//如果等于0,保持当前位置不变。
//大于0,放在对象后面。
//可以简单理解传入对象的位置就是0,如果返回为负数,则放在左边,如果返回正数,放在后面,如果为0,就不变。
if(this.money > other.money)
{
return 1;
}
else return -1;
return 0;
}
}
List<Item> itemlist = new List<Item>();
itemlist.Add(new Item(1));
itemlist.Add(new Item(1));
itemlist.Add(new Item(4));
itemlist.Add(new Item(5));
itemlist.Add(new Item(1));
itemlist.Add(new Item(4));
itemlist.Sort();

通过委托函数进行排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class ShopItem
{
public int id;
public ShopItem(int id)
{
this.id = id;
}
}
List<ShopList>shoplist = new List<ShopList>();
shopitem.Add(new ShopItem(1));
shopitem.Add(new ShopItem(1));
shopitem.Add(new ShopItem(4));
shopitem.Add(new ShopItem(5));
shopitem.Add(new ShopItem(1));
shopitem.Add(new ShopItem(4));
//配合函数使用
shopitem.Sort(Sortshopitem);
//配合匿名函数使用
shopitem.Sort(delegate (ShopItem a, ShopItem b)
{
if(a.id > b.id)
{
return 1;
}
else return -1;
return 0;
});
//配合Lambda表达式使用
shopitem.Sort((ShopItem a, ShopItem b) =>
{
return a.id > b.id ? 1 : -1;
});

static int Sortshopitem(ShopItem a, ShopItem b)
{
//传入的对象是列表中的两个对象,进行比较的时候,用左边的和右边的条件比较。
//返回值规则和之前一样。
if(a.id > b.id)
{
return 1;
}
else return -1;
return 0;
}

协变逆变

什么是协变逆变

协变:和谐的变化,自然的变化,因为里式替换原则中父类可以装子类,所以子类变父类比如string变成object感受起来是和谐的。

逆变:逆常规的变化,不正常的变化,因为里式替换原则中父类可以装子类,但是子类不能装父类,所以父类变子类如object变string感受是不和谐的。

协变和逆变是用来修饰泛型的;协变:out;逆变:in。

用于泛型中修饰泛型字母的,只有泛型接口和泛型委托能使用。

作用

返回值和参数

用out修饰的泛型,只能作为返回值。

1
delegate T Testout<out T>();

用in修饰的泛型,只能作为参数

1
delegate void Testin<in T>(T v);

结合里式替换原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
delegate T Testout<out T>();
delegate void Testin<in T>(T v);
class father
{

}
class son : father
{

}

Testout<son> os = () =>
{
return new son();
};
Testout<father> of = os;//加了out会自己判断返回值是否有父子关系,如果不加out仅仅只是两个普通的委托的话,这里会报错
father f = of();//实际上返回的是os里面装的函数返回的是son

Testin<father> IF = (v) =>
{

};
Testin<son> IS = IF;
IS(new son());//实际上调用的是IF,该处是子类装父类,为逆变
//协变逆变是在泛型委托和泛型接口里式替换原则中相互转换子类和父类相互装载的重要工具
//子类装父类是逆变,父类装子类是协变

总结:协变就是父类装子类,逆变就是子类装父类。(泛型委托)

多线程

进程

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
简单来说就是打开一个应用程序就是开启了一个进程,进程之间可以相互独立运行,互不干涉;进程之间也可以相互访问,操作。
打开电脑资源管理器我们就能看到一个个正在运行的前台进程和后台进程。

线程

操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,一条线程指的是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,目前我们写的程序都在主线程中。
简单理解就是代码从上到下运行的一条管道。

多线程

我们可以通过代码开启新的线程,可以同时运行代码的多条管道就叫多线程。

语法

c#提供了一个名为thread的多线程关键字。

声明

1
2
3
4
5
6
7
8
using System.Threading;

Thread t = new Thread(NewThreadLogic);//此处传入的就是新线程需要执行的代码语句。

static void NewThreadLogic()
{
新开线程执行的代码逻辑,在该函数语句块中。
}

启动线程

1
t.start();

设置为后台线程

后台线程:当前台线程结束了的时候,整个进程也就结束了,即使还有后台线程正在运行,后台线程不会防止应用程序的进程被终止,如果不设置为后台进程可能导致进程无法正常关闭。

1
2
3
4
5
6
7
8
static void NewThreadLogic()
{
新开线程执行的代码逻辑,在该函数语句块中。
while(1)
{

}//此时由于死循环,导致代码会无限执行下去,此时无法正常结束线程。
}
1
2
3
4
5
6
7
8
9
t.IsBackground = true;
static void NewThreadLogic()
{
新开线程执行的代码逻辑,在该函数语句块中。
while(1)
{

}//此时将新线程设置为后台线程,此时新线程不会干扰主线程的结束,主线程结束后,新线程也对应结束。
}

关闭线程

如果开启的线程中不是死循环,是能够提前结束的逻辑,那么就不用刻意去关闭它,但是如果是死循环的话,就必须终止这个线程,这里有两种方法

  • 死循环中bool标识
  • 通过线程提供方法(注意在.Net core 版本中无法终止会报错)
1
2
3
4
5
6
7
8
9
10
11
12
static bool IS = true;
t.start();
Console.ReadKey();
IS = false;
static void NewThreadLogic()
{
新开线程执行的代码逻辑,在该函数语句块中。
while(IS)
{

}
}//此时读入用户打的任意键后停止死循环,结束该线程。
1
2
3
4
5
6
7
8
9
10
11
12
static bool IS = true;
t.start();
t.Abort();
t = null;
static void NewThreadLogic()
{
新开线程执行的代码逻辑,在该函数语句块中。
while(IS)
{

}
}//先结束再置空即可关闭线程

线程休眠

1
2
3
Thread.Sleep(//此处写的是毫秒数);
//让线程暂停(休眠)多少毫秒。
//在哪个线程里写就在哪个线程里休眠。

线程之间共享数据

多个线程使用的内存是共享的,都是属于应用进程,所以要注意,当同时操作同一片内存区域可能出现问题,可以使用加锁的形式解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static bool IS = true;
t.start();
IS = false;
while(1)
{
Console.SetCursorPosition(0,0);

}

static void NewThreadLogic()
{
新开线程执行的代码逻辑,在该函数语句块中。
while(IS)
{
Console.SetCursorPosition(10,5);
}
}

此时有可能会出现问题。
使用关键字:lock就能解决。
当我们在多线程中想要访问同样的东西时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static bool IS = true;
object obj = new object();
t.start();
IS = false;
while(1)
{
lock(obj)//此时会判断该语句块中符合括号内的变量类型是否被锁住,如果被锁住就会先将锁住的内容完成后才会解锁才能在该语句块中执行。
{
Console.SetCursorPosition(0,0);
}
}

static void NewThreadLogic()
{
新开线程执行的代码逻辑,在该函数语句块中。
while(IS)
{
lock(obj)//与上面的锁一样,也就是说两个语句块中有关obj类型的数据总有一个先后顺序,不会出现同时进行而产生错误的情况。
{
Console.SetCursorPosition(10,5);
}
}
}

多线程对我们的意义

可以用多线程实现处理一些复杂的耗时的问题,比如寻路、网络通信等。

预处理器指令

编译器

编译器是一种翻译程序,它用于将源语言程序翻译成目标语言程序。
源语言程序:某种程序设计语言写成的,比如c,c#,c++等等。
目标语言程序:二进制数表示的伪机器代码的程序。

预处理器指令

指导编译器在实际编译开始前对信息进行预处理。
预处理器指令都是以#开头
预处理器指令不是语句,不适用;结束。
目前我们使用的头文件就是和折叠代码块就是预处理器指令。

常见的预处理器指令

define

定义一个符号,类似一个没有值的变量。
#undef 取消define定义的符号,让其失效。
两者都是写在脚本文件最前面的,一般配合if使用。

1
#define Unity

if

与if语句规则一样,一般配合define使用。

1
2
3
4
5
6
7
#if Unity
//如果发现有Unity这个符号存在,则会执行相应语句块
#elif Unity4
//同理,如果上述没有Unity执行才会进行判断elif
#else
//......
#endlf

warning和error

告诉编译器是警告还是报错。
一般配合if使用。

1
2
3
4
5
#if Unity
//如果发现有Unity这个符号存在,则会执行相应语句块
#warning 警告
#error 错误
#endlf